﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Rob Ashton 2009
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@mvcex.com
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using System.Web.Mvc;

namespace MvcEx.Controllers
{
    /// <summary>
    /// The controller builder for a specific configuration
    /// </summary>
    public class MvcExControllerBuilder : IMvcExControllerBuilder
    {
        #region Static Fields

        private static int sCurrentIndex = 0;
        private static Object sCurrentIndexLock = new object();

        private static int GetBuilderIndex()
        {
            lock (sCurrentIndexLock)
            {
                int index = sCurrentIndex;
                sCurrentIndex++;
                return index;
            }
        }

        #endregion

        #region Instance Fields

        private Assembly[] mAssemblies;
        private Dictionary<String, Type> mControllerTypes = new Dictionary<string, Type>();
        private AssemblyBuilder mAssemblyBuilder;
        private ModuleBuilder mModuleBuilder;
        private String mIdentifier;

        #endregion

        #region .ctor

        public MvcExControllerBuilder(MvcExControllerBuilderConfiguration configuration)
        {
            // Copy out the assemblies
            mAssemblies = configuration.Assemblies;
            mIdentifier = configuration.BuilderIdentifier;

            // Get an index to use on the end of the dynamically generated type
            int builderIndex = GetBuilderIndex();

            // Create the dynamic assembly within the current domain
            mAssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
                new AssemblyName(string.Format("DynamicControllerAssembly{0}{1}", mIdentifier, builderIndex)),
                AssemblyBuilderAccess.RunAndSave);

            // And define a module to hold our created types
            mModuleBuilder = mAssemblyBuilder.DefineDynamicModule(string.Format("DynamicControllerModule{0}{1}", mIdentifier, builderIndex));
        }

        #endregion

        #region IMvcExControllerBuilder Implementations

        public Type GetControllerType(String controllerName)
        {
            return GetControllerType(controllerName, typeof(MvcExController));
        }

        public Type GetControllerType(String controllerName, Type baseControllerType)
        {
            string key = String.Format("{0}{1}", controllerName, baseControllerType.FullName);

            // Create the type if we haven't got it already
            Type controllerType = null;
            if (!mControllerTypes.TryGetValue(key, out controllerType))
            {
                controllerType = CreateType(controllerName, baseControllerType);

                // And then store it (even if it's null)
                mControllerTypes[key] = controllerType;
            }
            return controllerType;
        }

        #endregion

        #region Private Worker Methods

        private Type CreateType(String controllerName, Type baseControllerType)
        {
            int index = GetBuilderIndex();

            // We need to create a new class which will be called <ControllerName>Controller
            TypeBuilder controllerType = mModuleBuilder.DefineType(String.Format("{0}{1}Controller", controllerName, index));

            // Which inherits PluggableController
            controllerType.SetParent(baseControllerType);

            // Create the constructor
            CreateConstructors(controllerType);

            // Find all the types that need methods adding on this beast
            var actionContainerTypes = FindControllerActionContainers(controllerName);

            // Could not create it
            if (actionContainerTypes.Length == 0) { return null; }

            // Reverse the order of types in this list to make last-added the priority when resolving action names
            actionContainerTypes.Reverse();

            // Now go through all those types
            foreach (var type in actionContainerTypes)
            {
                // And all the methods on that type
                foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
                {
                    CreateMethodOnDynamicController(controllerType, method);
                }
            }
            // Return the created type
            return controllerType.CreateType();
        }

        private void CreateConstructors(TypeBuilder controllerType)
        {
            // Define the constructor
            var iocConstructor = controllerType.DefineConstructor(
                MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, 
                CallingConventions.Standard, 
                new Type[] {typeof(IIocContainer)});

            // Get the base type constructor
            var baseParamConstructor = typeof(MvcExController).GetConstructor(new Type[] { typeof(IIocContainer) });

            var ilGen = iocConstructor.GetILGenerator();
            ilGen.Emit(OpCodes.Ldarg_0);
            ilGen.Emit(OpCodes.Ldarg, 1);
            ilGen.Emit(OpCodes.Call, baseParamConstructor);
            ilGen.Emit(OpCodes.Ret);

            // And define a default constructor
            var defaultConstructor = controllerType.DefineDefaultConstructor(MethodAttributes.Public);
        }

        private void CreateMethodOnDynamicController(TypeBuilder controllerType, MethodInfo methodInfo)
        {
            // Get a list of all the objects this method takes in as a parameter
            List<Type> paramTypes = new List<Type>();
            var methodParams =  methodInfo.GetParameters();
            foreach (var param in methodParams)
            {
                paramTypes.Add(param.ParameterType);
            }

            // Get the greediest constructor of the type containing the method info
            var containerConstructor = methodInfo.DeclaringType.GetConstructors()
                            .OrderBy(x => x.GetParameters().Length)
                            .Take(1).Single();

            // And the initializion method of BaseActionContainer
            var initializeMethod = typeof(MvcExController).GetMethod("RegisterContainer", BindingFlags.Public | BindingFlags.Instance);

            // Create the method builder which takes all of those parameters in that order
            var methodBuilder = controllerType.DefineMethod(
                methodInfo.Name,
                MethodAttributes.Public,
                CallingConventions.HasThis,
                typeof(ActionResult),
                paramTypes.ToArray()
                );

            // Define the parametrs
            for(int x = 1 ; x <= methodParams.Length ; x++)
            {
                methodBuilder.DefineParameter(x, ParameterAttributes.In, methodParams[x-1].Name);
            }

            // Copy the attributes across from the original method
            CopyActionMethodAttributes(methodInfo, methodBuilder);

            // And get the il generator
            var ilGen = methodBuilder.GetILGenerator();

            // Define a local variable for holding the instance of the action container
            var actionContainer = ilGen.DeclareLocal(typeof(MvcExActionContainer));
            
            // Define locals for each of the arguments that the constuctor needs
            List<LocalBuilder> constructorArgs = new List<LocalBuilder>();
            LocalBuilder localType = ilGen.DeclareLocal(typeof(Type));

            foreach (var parameter in containerConstructor.GetParameters())
            {
                // Declare the local
                var parameterLocal = ilGen.DeclareLocal(parameter.ParameterType);

                // Get the method we call to resolve this type
                var resolutionMethod = typeof(MvcExController).GetMethod("ResolveType", BindingFlags.NonPublic | BindingFlags.Instance);
                var debugMethod = typeof(MvcExController).GetMethod("DebugMethod", BindingFlags.NonPublic | BindingFlags.Instance);

                // We need to call GetType in order to have an argument for that method
                var getTypeMethod = typeof(Type).GetMethod("GetType", 
                            BindingFlags.Static | BindingFlags.Public, 
                            null,
                            new Type[] { typeof(String)},
                            null                            
                            );
            
                 // We then load a string containing the fully qualified type we're looking for
                ilGen.Emit(OpCodes.Ldstr, parameter.ParameterType.AssemblyQualifiedName);

                // And call get type, storing the result in the temporary var we have
                ilGen.Emit(OpCodes.Call, getTypeMethod);
                ilGen.Emit(OpCodes.Stloc, localType.LocalIndex);
               
                // Now we call the resolution method
                ilGen.Emit(OpCodes.Ldarg_0);
                ilGen.Emit(OpCodes.Ldloc, localType.LocalIndex);
                ilGen.Emit(OpCodes.Callvirt, resolutionMethod);

                // And store the result on the local
                ilGen.Emit(OpCodes.Stloc, parameterLocal.LocalIndex);

                // Store it in the list for when we call that constructor
                constructorArgs.Add(parameterLocal);
            }

            // Load all the arguments for the constructor onto the stack
            foreach (var local in constructorArgs)
            {
                ilGen.Emit(OpCodes.Ldloc, local.LocalIndex);
            }

            // Load the type of the action container for this method
            ilGen.Emit(OpCodes.Newobj, containerConstructor);

            // Set the value of the local variable with the result of calling the constructor
            ilGen.Emit(OpCodes.Stloc, actionContainer);

            // Call the initialize method so we init the action container
            ilGen.Emit(OpCodes.Ldarg_0);
            ilGen.Emit(OpCodes.Ldloc, actionContainer.LocalIndex);

            // And invoke the initialize method
            ilGen.Emit(OpCodes.Callvirt, initializeMethod);

            // Now load up the action container one more time so we can call the method we were originally invoked for
            ilGen.Emit(OpCodes.Ldloc, actionContainer.LocalIndex);

            // Load all of the arguments as arguments for that action
            for (int x = 0; x < paramTypes.Count; x++)
            {
                ilGen.Emit(OpCodes.Ldarg, x + 1);
            }

            // And call the relevant pass through method
            ilGen.Emit(OpCodes.Callvirt, methodInfo);

            // We return the return value of that method, so we leave
            // The return value on the stack and just exit
            ilGen.Emit(OpCodes.Ret);
        }

        private void CopyActionMethodAttributes(MethodInfo methodInfo, MethodBuilder methodBuilder)
        {
            // We need to copy all the attributes across from the method we're wrapping up
            foreach (var attribute in methodInfo.GetCustomAttributes(true))
            {
                // Which means we can create an attribute builder
                Type attributeType = attribute.GetType();
                CustomAttributeBuilder attributeBuilder = null;

                // AcceptVerbs doesn't have an empty constructor
                if (attributeType == typeof(AcceptVerbsAttribute))
                {
                    // So do it the custom way
                    attributeBuilder = CopyAcceptVerbsAttribute(attribute, attributeType);                      
                }
                else if (attributeType == typeof(ValidateInputAttribute))
                {
                    // And this one
                    attributeBuilder = CopyValidateInputAttribute(attribute, attributeType);
                }
                else
                {
                    // Else, we create the builder using the default constructor and perform property copying
                    attributeBuilder = CopyDefaultAttributeByPropertyMapping(attribute, attributeType);
                }

                // And add this attribute to the new method
                methodBuilder.SetCustomAttribute(attributeBuilder);
            
            }
        }

        private CustomAttributeBuilder CopyValidateInputAttribute(object attribute, Type attributeType)
        {
            CustomAttributeBuilder attributeBuilder = null;

            // Get the constructor which takes in an array of strings
            ConstructorInfo validateInputCtor = attributeType.GetConstructor(new Type[] { typeof(bool) });

            // Get the ValidateInputAttribute attribute
            ValidateInputAttribute validateInputAttr = attribute as ValidateInputAttribute;

            bool arg = validateInputAttr.EnableValidation;

            // And pass the array of strings to the constructor
            attributeBuilder = new CustomAttributeBuilder(
                    validateInputCtor, new Object[] { arg }
                    );

            return attributeBuilder;
        }

        private CustomAttributeBuilder CopyAcceptVerbsAttribute(object attribute, Type attributeType)
        {
            CustomAttributeBuilder attributeBuilder = null;

            // Get the constructor which takes in an array of strings
            ConstructorInfo acceptVerbsCtor = attributeType.GetConstructor(new Type[] { typeof(String[]) });

            // Get the typed attribute
            AcceptVerbsAttribute acceptVerbsAttr = attribute as AcceptVerbsAttribute;

            String[] args = acceptVerbsAttr.Verbs.ToArray();

            // And pass the array of strings to the constructor
            attributeBuilder = new CustomAttributeBuilder(
                    acceptVerbsCtor, new Object[] { args }
                    );  

            return attributeBuilder;
        }

        private  CustomAttributeBuilder CopyDefaultAttributeByPropertyMapping(object attribute, Type attributeType)
        {

            CustomAttributeBuilder attributeBuilder = null;

            // This means getting all the properties of that attribute
            PropertyInfo[] attributeProperties = attribute.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                                .Where(x => x.CanWrite)
                                                .ToArray();

            // And all the values of that particular property
            Object[] values = new Object[attributeProperties.Length];

            for (int x = 0; x < values.Length; x++)
            {
                PropertyInfo property = attributeProperties[x];
                values[x] = property.GetValue(attribute, null);
            }

            // Perform the property copying
            attributeBuilder = new CustomAttributeBuilder(
                                            attributeType.GetConstructor(Type.EmptyTypes),
                                            new Object[] { },
                                            attributeProperties,
                                            values);
            return attributeBuilder;
        }

        private Type[] FindControllerActionContainers(String controllerName)
        {
            // This is the list of types we'll be returning
            List<Type> types = new List<Type>();

            // Go through each assembly
            foreach (var assembly in mAssemblies)
            {
                // Get all the types
                var assemblyTypes = from type in assembly.GetTypes()
                                    where
                                        (
                                        // Where they have an attribute of ActionContainerAttribute
                                            from attr in type.GetCustomAttributes(typeof(MvcExActionContainerAttribute), true)

                                            // And the controller name is the one we're looking for
                                            where String.Compare(((MvcExActionContainerAttribute)attr).ControllerName, controllerName, true) == 0 ||
                                            String.Compare(((MvcExActionContainerAttribute)attr).ControllerName, "shared", true) == 0

                                            // This is just used for a count
                                            select attr

                                        )
                                        // Because as long as it has it, we don't care, it's valid
                                        .Count() > 0

                                    // Cos now we have the type
                                    select type;

                // Add to the pile of located types
                types.AddRange(assemblyTypes);
            }

            // And return the located types
            return types.ToArray();
        }

        #endregion
    }
}
